Ce tutorial présente le système de coordonnées 2D en C# Windows Forms (.NET 8+) à travers la création d'une course d'escargots animée.
Nous allons créer un jeu graphique où :
À travers 4 étapes progressives, vous apprendrez à :
Graphics (Rectangle, String, Point)BufferedGraphics pour une animation fluideEn Windows Forms, comme en console :
(0,0)─────────────────→ X
│
│ ┌─────────┐
│ │ Form │
│ │ │
│ └─────────┘
↓
Y
X | Distance depuis le bord gauche |
Y | Distance depuis le bord haut |
(0,0) | Coin supérieur gauche |
Ouvrir Form1.cs et remplacer tout le contenu par :
namespace SnailRace;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
// Configuration de la fenêtre
this.Text = "Snail Race - Coordonnées 2D";
this.Size = new Size(800, 400);
this.BackColor = Color.Black;
// Événement pour dessiner
this.Paint += Form1_Paint;
}
private void Form1_Paint(object? sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
// Point A en haut à gauche (près de l'origine)
g.FillEllipse(Brushes.Red, 10, 10, 20, 20);
g.DrawString("A (10,10)", this.Font, Brushes.White, 35, 10);
// Point B en haut à droite
g.FillEllipse(Brushes.Green, 700, 10, 20, 20);
g.DrawString("B (700,10)", this.Font, Brushes.White, 640, 10);
// Point C en bas à gauche
g.FillEllipse(Brushes.Blue, 10, 300, 20, 20);
g.DrawString("C (10,300)", this.Font, Brushes.White, 35, 300);
// Point D en bas à droite
g.FillEllipse(Brushes.Yellow, 700, 300, 20, 20);
g.DrawString("D (700,300)", this.Font, Brushes.White, 620, 300);
// Lignes pour montrer les axes
g.DrawLine(Pens.Gray, 0, 0, 780, 0); // Axe X (haut)
g.DrawLine(Pens.Gray, 0, 0, 0, 380); // Axe Y (gauche)
}
}

g.FillEllipse(Brushes.Red, 10, 10, 20, 20);
// X Y W H
// │ │ │ └── Hauteur
// │ │ └────── Largeur
// │ └────────── Y (depuis le haut)
// └────────────── X (depuis la gauche)
Modifier le code pour dessiner un rectangle au centre de la fenêtre (environ 400, 180).
Créer une fonction pour dessiner un escargot à une position donnée.
Notre escargot sera composé de :
┌───┐
│ 1 ├──●
└───┘
Corps Coquille
namespace SnailRace;
public partial class Form1 : Form
{
// Taille de l'escargot
const int SNAIL_WIDTH = 40;
const int SNAIL_HEIGHT = 25;
public Form1()
{
InitializeComponent();
this.Text = "Snail Race";
this.Size = new Size(800, 400);
this.BackColor = Color.FromArgb(30, 30, 30);
this.Paint += Form1_Paint;
}
private void Form1_Paint(object? sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
// Dessiner plusieurs escargots à différentes positions
DrawSnail(g, 50, 50, 1, Color.Yellow);
DrawSnail(g, 50, 100, 2, Color.Cyan);
DrawSnail(g, 50, 150, 3, Color.Magenta);
DrawSnail(g, 50, 200, 4, Color.Lime);
}
private void DrawSnail(Graphics g, int x, int y, int number, Color color)
{
// Corps (rectangle)
using (Brush bodyBrush = new SolidBrush(color))
{
g.FillRectangle(bodyBrush, x, y, SNAIL_WIDTH, SNAIL_HEIGHT);
}
// Contour du corps
using (Pen outlinePen = new Pen(Color.White, 1))
{
g.DrawRectangle(outlinePen, x, y, SNAIL_WIDTH, SNAIL_HEIGHT);
}
// Coquille (cercle à droite du corps)
int shellX = x + SNAIL_WIDTH - 5;
int shellY = y + 2;
int shellSize = SNAIL_HEIGHT - 4;
using (Brush shellBrush = new SolidBrush(Color.FromArgb(150, color)))
{
g.FillEllipse(shellBrush, shellX, shellY, shellSize, shellSize);
}
g.DrawEllipse(Pens.White, shellX, shellY, shellSize, shellSize);
// Numéro de l'escargot
using (Font font = new Font("Arial", 10, FontStyle.Bold))
{
string text = number.ToString();
SizeF textSize = g.MeasureString(text, font);
float textX = x + (SNAIL_WIDTH - textSize.Width) / 2;
float textY = y + (SNAIL_HEIGHT - textSize.Height) / 2;
g.DrawString(text, font, Brushes.Black, textX, textY);
}
}
}
usingusing (Brush bodyBrush = new SolidBrush(color))
{
g.FillRectangle(bodyBrush, x, y, SNAIL_WIDTH, SNAIL_HEIGHT);
}
Les objets graphiques (Brush, Pen, Font) consomment des ressources. Le using garantit leur libération automatique.
SizeF textSize = g.MeasureString(text, font);
float textX = x + (SNAIL_WIDTH - textSize.Width) / 2;
float textY = y + (SNAIL_HEIGHT - textSize.Height) / 2;
MeasureString permet de connaître la taille du texte avant de le dessiner.
Ajouter deux "antennes" à l'escargot avec g.DrawLine().
Animer les escargots avec un affichage fluide grâce au double-buffering.
Sans double-buffering, l'animation scintille car :
Le double-buffering dessine d'abord dans une image en mémoire, puis affiche l'image complète d'un coup.
┌──────────┐ ┌────────────┐
│ Buffer │ ──► │ Écran │
│ (dessin) │ │(affichage) │
└──────────┘ └────────────┘
namespace SnailRace;
public partial class Form1 : Form
{
// Configuration
const int SNAIL_WIDTH = 40;
const int SNAIL_HEIGHT = 25;
const int NUM_SNAILS = 4;
const int TRACK_START = 60;
const int TRACK_END = 700;
// Données des escargots (tableaux parallèles)
int[] snailX = new int[NUM_SNAILS];
int[] snailY = new int[NUM_SNAILS];
int[] snailEnergy = new int[NUM_SNAILS];
Color[] snailColors = { Color.Yellow, Color.Cyan, Color.Magenta, Color.Lime };
// Double buffering
BufferedGraphicsContext context;
BufferedGraphics buffer;
// Animation
System.Windows.Forms.Timer gameTimer;
Random random = new Random();
bool raceStarted = false;
public Form1()
{
InitializeComponent();
this.Text = "Snail Race - Press SPACE to start";
this.Size = new Size(800, 300);
this.BackColor = Color.FromArgb(30, 30, 30);
this.DoubleBuffered = true; // Active aussi le double-buffering du Form
// Initialiser les escargots
for (int i = 0; i < NUM_SNAILS; i++)
{
snailX[i] = TRACK_START;
snailY[i] = 50 + i * 50; // Espacement vertical
snailEnergy[i] = 100;
}
// Configurer le double-buffering manuel
context = BufferedGraphicsManager.Current;
context.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
// Timer pour l'animation (60 FPS environ)
gameTimer = new System.Windows.Forms.Timer();
gameTimer.Interval = 16; // ~60 FPS
gameTimer.Tick += GameTimer_Tick;
// Événements
this.Paint += Form1_Paint;
this.KeyDown += Form1_KeyDown;
this.Resize += Form1_Resize;
}
private void Form1_Resize(object? sender, EventArgs e)
{
// Recréer le buffer si la fenêtre change de taille
context.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
}
private void Form1_KeyDown(object? sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
if (!raceStarted)
{
raceStarted = true;
gameTimer.Start();
this.Text = "Snail Race - GO!";
}
}
if (e.KeyCode == Keys.R)
{
// Reset
ResetRace();
}
}
private void ResetRace()
{
gameTimer.Stop();
raceStarted = false;
for (int i = 0; i < NUM_SNAILS; i++)
{
snailX[i] = TRACK_START;
snailEnergy[i] = 100;
}
this.Text = "Snail Race - Press SPACE to start";
this.Invalidate();
}
private void GameTimer_Tick(object? sender, EventArgs e)
{
bool raceFinished = false;
int winner = -1;
// Déplacer chaque escargot
for (int i = 0; i < NUM_SNAILS; i++)
{
// Probabilité d'avancer basée sur l'énergie
if (random.Next(100) < snailEnergy[i])
{
snailX[i] += random.Next(1, 4); // Avance de 1-3 pixels
// Fatigue
snailEnergy[i] -= random.Next(1, 3);
if (snailEnergy[i] < 10) snailEnergy[i] = 10;
}
// Vérifier victoire
if (snailX[i] >= TRACK_END)
{
raceFinished = true;
winner = i;
break;
}
}
// Redessiner
this.Invalidate();
if (raceFinished)
{
gameTimer.Stop();
this.Text = $"Snail {winner + 1} WINS! Press R to restart";
}
}
private void Form1_Paint(object? sender, PaintEventArgs e)
{
// Créer le buffer
buffer = context.Allocate(e.Graphics, this.ClientRectangle);
Graphics g = buffer.Graphics;
// Effacer avec la couleur de fond
g.Clear(this.BackColor);
// Activer l'anti-aliasing pour un rendu plus lisse
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// Dessiner les pistes
DrawTracks(g);
// Dessiner les escargots
for (int i = 0; i < NUM_SNAILS; i++)
{
DrawSnail(g, snailX[i], snailY[i], i + 1, snailColors[i]);
DrawEnergyBar(g, i);
}
// Dessiner la ligne d'arrivée
DrawFinishLine(g);
// Afficher le buffer sur l'écran
buffer.Render(e.Graphics);
buffer.Dispose();
}
private void DrawTracks(Graphics g)
{
using (Pen trackPen = new Pen(Color.FromArgb(60, 60, 60), 30))
{
for (int i = 0; i < NUM_SNAILS; i++)
{
int y = snailY[i] + SNAIL_HEIGHT / 2;
g.DrawLine(trackPen, TRACK_START, y, TRACK_END, y);
}
}
// Ligne de départ
using (Pen startPen = new Pen(Color.White, 3))
{
g.DrawLine(startPen, TRACK_START, 40, TRACK_START, 40 + NUM_SNAILS * 50);
}
}
private void DrawFinishLine(Graphics g)
{
// Damier d'arrivée
int squareSize = 10;
for (int row = 0; row < (NUM_SNAILS * 50) / squareSize; row++)
{
for (int col = 0; col < 2; col++)
{
bool isWhite = (row + col) % 2 == 0;
Brush brush = isWhite ? Brushes.White : Brushes.Black;
g.FillRectangle(brush,
TRACK_END + col * squareSize,
40 + row * squareSize,
squareSize, squareSize);
}
}
}
private void DrawEnergyBar(Graphics g, int snailIndex)
{
int barX = 10;
int barY = snailY[snailIndex] + 5;
int barWidth = 40;
int barHeight = 15;
// Fond de la barre
g.FillRectangle(Brushes.DarkGray, barX, barY, barWidth, barHeight);
// Niveau d'énergie
int energyWidth = (int)(barWidth * snailEnergy[snailIndex] / 100.0);
Color energyColor = snailEnergy[snailIndex] > 50 ? Color.LimeGreen :
snailEnergy[snailIndex] > 25 ? Color.Orange : Color.Red;
using (Brush energyBrush = new SolidBrush(energyColor))
{
g.FillRectangle(energyBrush, barX, barY, energyWidth, barHeight);
}
// Contour
g.DrawRectangle(Pens.White, barX, barY, barWidth, barHeight);
}
private void DrawSnail(Graphics g, int x, int y, int number, Color color)
{
// Corps
using (Brush bodyBrush = new SolidBrush(color))
{
g.FillRectangle(bodyBrush, x, y, SNAIL_WIDTH, SNAIL_HEIGHT);
}
g.DrawRectangle(Pens.White, x, y, SNAIL_WIDTH, SNAIL_HEIGHT);
// Coquille
int shellX = x + SNAIL_WIDTH - 5;
int shellY = y + 2;
int shellSize = SNAIL_HEIGHT - 4;
using (Brush shellBrush = new SolidBrush(Color.FromArgb(180, color)))
{
g.FillEllipse(shellBrush, shellX, shellY, shellSize, shellSize);
}
g.DrawEllipse(Pens.White, shellX, shellY, shellSize, shellSize);
// Numéro
using (Font font = new Font("Arial", 9, FontStyle.Bold))
{
string text = number.ToString();
SizeF textSize = g.MeasureString(text, font);
float textX = x + (SNAIL_WIDTH - textSize.Width) / 2;
float textY = y + (SNAIL_HEIGHT - textSize.Height) / 2;
g.DrawString(text, font, Brushes.Black, textX, textY);
}
}
}
// 1. Créer le contexte (une fois)
context = BufferedGraphicsManager.Current;
// 2. Dans Paint, allouer un buffer
buffer = context.Allocate(e.Graphics, this.ClientRectangle);
Graphics g = buffer.Graphics;
// 3. Dessiner dans le buffer
g.Clear(this.BackColor);
// ... tous les dessins ...
// 4. Afficher le buffer d'un coup
buffer.Render(e.Graphics);
buffer.Dispose();
gameTimer = new System.Windows.Forms.Timer();
gameTimer.Interval = 16; // Millisecondes entre chaque tick
gameTimer.Tick += GameTimer_Tick; // Fonction appelée à chaque tick
gameTimer.Start(); // Démarre l'animation
16 ms | ~60 FPS |
33 ms | ~30 FPS |
100 ms | 10 FPS |
Permettre au joueur d'encourager son escargot pendant la course.
SPACE (avant course) | Démarrer la course |
SPACE (pendant course) | Booster l'escargot du joueur |
R | Recommencer |
Modifier les parties suivantes du code :
// Ajouter cette variable en haut de la classe
int playerSnailIndex = 0; // Le joueur contrôle l'escargot 0
int boostCooldown = 0; // Temps avant de pouvoir rebooster
// Modifier Form1_KeyDown
private void Form1_KeyDown(object? sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
if (!raceStarted)
{
// Démarrer la course
raceStarted = true;
gameTimer.Start();
this.Text = "Snail Race - SPACE to boost!";
}
else if (boostCooldown <= 0)
{
// Booster l'escargot du joueur
snailEnergy[playerSnailIndex] += 15;
if (snailEnergy[playerSnailIndex] > 100)
snailEnergy[playerSnailIndex] = 100;
boostCooldown = 10; // Cooldown de 10 frames
}
}
if (e.KeyCode == Keys.R)
{
ResetRace();
}
}
// Modifier GameTimer_Tick pour gérer le cooldown
private void GameTimer_Tick(object? sender, EventArgs e)
{
// Réduire le cooldown
if (boostCooldown > 0) boostCooldown--;
bool raceFinished = false;
int winner = -1;
for (int i = 0; i < NUM_SNAILS; i++)
{
if (random.Next(100) < snailEnergy[i])
{
snailX[i] += random.Next(1, 4);
snailEnergy[i] -= random.Next(1, 3);
if (snailEnergy[i] < 10) snailEnergy[i] = 10;
}
if (snailX[i] >= TRACK_END)
{
raceFinished = true;
winner = i;
break;
}
}
this.Invalidate();
if (raceFinished)
{
gameTimer.Stop();
string result = (winner == playerSnailIndex) ? "YOU WIN!" : $"Snail {winner + 1} wins...";
this.Text = $"{result} Press R to restart";
}
}
// Modifier ResetRace
private void ResetRace()
{
gameTimer.Stop();
raceStarted = false;
boostCooldown = 0;
for (int i = 0; i < NUM_SNAILS; i++)
{
snailX[i] = TRACK_START;
snailEnergy[i] = 100;
}
this.Text = "Snail Race - Press SPACE to start";
this.Invalidate();
}
Modifier DrawSnail pour marquer l'escargot du joueur :
private void DrawSnail(Graphics g, int x, int y, int number, Color color, bool isPlayer = false)
{
// Indicateur "YOU" pour le joueur
if (isPlayer)
{
using (Font smallFont = new Font("Arial", 7))
{
g.DrawString("YOU", smallFont, Brushes.White, x, y - 12);
}
// Contour plus épais pour le joueur
using (Pen playerPen = new Pen(Color.Gold, 3))
{
g.DrawRectangle(playerPen, x - 2, y - 2, SNAIL_WIDTH + 4, SNAIL_HEIGHT + 4);
}
}
// Corps
using (Brush bodyBrush = new SolidBrush(color))
{
g.FillRectangle(bodyBrush, x, y, SNAIL_WIDTH, SNAIL_HEIGHT);
}
g.DrawRectangle(Pens.White, x, y, SNAIL_WIDTH, SNAIL_HEIGHT);
// ... reste du code identique ...
}
Et modifier l'appel dans Form1_Paint :
for (int i = 0; i < NUM_SNAILS; i++)
{
bool isPlayer = (i == playerSnailIndex);
DrawSnail(g, snailX[i], snailY[i], i + 1, snailColors[i], isPlayer);
DrawEnergyBar(g, i);
}
Ajouter dans Form1_Paint après les escargots :
// Indicateur de boost disponible
if (raceStarted)
{
string boostText = boostCooldown > 0 ? $"Boost: {boostCooldown}" : "BOOST READY!";
Color boostColor = boostCooldown > 0 ? Color.Gray : Color.LimeGreen;
using (Font boostFont = new Font("Arial", 12, FontStyle.Bold))
using (Brush boostBrush = new SolidBrush(boostColor))
{
g.DrawString(boostText, boostFont, boostBrush, 10, this.ClientSize.Height - 30);
}
}
Windows Forms n'a pas de DrawPoint direct, mais on peut utiliser FillRectangle avec 1x1 pixel ou FillEllipse :
// Point simple (1 pixel)
g.FillRectangle(Brushes.White, x, y, 1, 1);
// Point visible (petit cercle)
void DrawPoint(Graphics g, int x, int y, Color color, int size = 4)
{
using (Brush brush = new SolidBrush(color))
{
g.FillEllipse(brush, x - size/2, y - size/2, size, size);
}
}
// Ajouter un tableau pour stocker les positions précédentes
Queue<Point>[] snailTrails = new Queue<Point>[NUM_SNAILS];
// Dans l'initialisation
for (int i = 0; i < NUM_SNAILS; i++)
{
snailTrails[i] = new Queue<Point>();
}
// Dans GameTimer_Tick, avant de déplacer
snailTrails[i].Enqueue(new Point(snailX[i], snailY[i]));
if (snailTrails[i].Count > 10) snailTrails[i].Dequeue();
// Dans le dessin
void DrawTrail(Graphics g, int snailIndex, Color color)
{
int alpha = 50;
foreach (Point p in snailTrails[snailIndex])
{
using (Brush trailBrush = new SolidBrush(Color.FromArgb(alpha, color)))
{
g.FillEllipse(trailBrush, p.X + 20, p.Y + 10, 8, 8);
}
alpha += 15;
}
}
void DrawConfetti(Graphics g, int winnerIndex)
{
for (int i = 0; i < 50; i++)
{
int x = random.Next(this.ClientSize.Width);
int y = random.Next(this.ClientSize.Height);
Color c = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256));
using (Brush brush = new SolidBrush(c))
{
g.FillRectangle(brush, x, y, 5, 5);
}
}
}
Origine | (0,0) haut-gauche | (0,0) haut-gauche |
Unité | Caractères | Pixels |
Y augmente | Vers le bas | Vers le bas |
Positionnement |
| Paramètres (x, y) |
Dessin | Caractères ASCII |
|
| Rectangle plein |
| Contour rectangle |
| Ellipse/cercle plein |
| Contour ellipse |
| Texte |
| Ligne |
System.Windows.Forms.Timer pour les jeux simplespublic class Snail
{
public int X { get; set; }
public int Y { get; set; }
public int Energy { get; set; }
public Color Color { get; set; }
public void Draw(Graphics g) { ... }
public void Move(Random random) { ... }
public void Boost() { ... }
}
public class Race
{
private List<Snail> snails;
private BufferedGraphics buffer;
public void Update() { ... }
public void Draw(Graphics g) { ... }
}